<?php

class Indexer_Service
{
    public    $encoding = 'UTF-8';
    protected $dataDir = '';
    protected $catalog = 'default';
    /**
     * @var Zend_Search_Lucene
     */
    protected $index = null;
    
    public static $logger    = null;
    public static $logTarget = null;
    
    public function __construct($catalog='default')
    {
        // this should be a default, but we'll set it anyway in case this ever changes
        Zend_Search_Lucene::setDefaultSearchField(null); // search all fields
        
        $this->catalog = $catalog;
        $this->dataDir = dirname(__FILE__).'/../../data/'.$catalog.'/';
        $this->dataDir = realpath($this->dataDir);
        //self::getLog()->debug('real path for data dir: '.$this->dataDir);
    }
    
    /**
     * Opens a search index
     *
     * @return Zend_Search_Lucene
     */
    protected function _openIndex()
    {
        self::getLog()->debug('trying to access data dir: '.$this->dataDir);
        if (!($this->index instanceof Zend_Search_Lucene_Interface)) {
            // Open existing index
            try {
                $this->index = Zend_Search_Lucene::open($this->dataDir);
            } catch(Exception $ex) {
                throw new Exception("Catalog '{$this->catalog}' does not exist. Message: ".$ex->getMessage());
            }
        }
        return $this->index;
    }
    
    /**
     * Explicitly opens a search index. Offers option to automatically
     * create a new index if none is found (default). Also allows overriding
     * the preset data path if specified. 
     *
     * @param bool $createIfNotExists Default: true
     * @param mixed $overridePath Boolean false (default) or string with new data path
     */
    public function openIndex($createIfNotExists = true, $overridePath = false)
    {
        self::getLog()->debug('Opening search index...');
        if ($overridePath !== false) {
            self::getLog()->debug('Overriding data path with: ' . $overridePath);
            $this->dataDir = $overridePath;
        }
        try {
            $this->_openIndex();
        } catch(Exception $ex) {
            if ($createIfNotExists === true) {
                self::getLog()->warn('Opening search index failed, trying to create new one');
                $this->createNewIndex();
                $this->_openIndex();
            } else {
                throw $ex;
            }
        }
    }
    
    /**
     * Returns a singleton log object
     *
     * @return Zend_Log
     */
    public static function getLog()
    {
        if (!(self::$logger instanceof Zend_Log)) {
            if (empty(self::$logTarget)) {
                self::$logTarget = ini_get('error_log');
            }
            $writer = new Zend_Log_Writer_Stream(self::$logTarget);
            $format = '%timestamp% [%ip%] %priorityName%: %message%' . PHP_EOL;
            $writer->setFormatter(new Zend_Log_Formatter_Simple($format));
            self::$logger = new Zend_Log($writer);
            if (!empty($_SERVER['REMOTE_ADDR'])) {
                self::$logger->setEventItem('ip', $_SERVER['REMOTE_ADDR']);
            } else {
                self::$logger->setEventItem('ip', 'n/a');
            }
        }
        return self::$logger;

    }
    
    /**
     * Creates a new index if it does not exist. Uses catalog name
     * given in the constructor.
     * Returns true if operation successful, false if not.
     *
     * @return boolean
     */
    public function createNewIndex() 
    {
        // Create temporary index
        if (empty($this->dataDir)) {
            // could be that this is empty even after setting in constructor,
            // as realpath() returns empty string if directory doesn't exist
            $this->dataDir = dirname(__FILE__).'/../../data/'.$this->catalog.'/';
        }
        if (!file_exists($this->dataDir)) {
            mkdir($this->dataDir, 0775);
            self::getLog()->debug('createNewIndex: Creating new index at ' . $this->dataDir);
            $this->index = Zend_Search_Lucene::create($this->dataDir);
            return true;
        } else {
            self::getLog()->debug('createNewIndex: Catalog dir already exists: ' . $this->dataDir);
            return false;
        }
    }
    
    /**
     * Searches the current catalogue for the given search query.
     * 
     * @param string  $q     The search query
     * @param string  $wt    The ResponseWriter to use (default: XML)
     * @param string  $fl    Comma-separated list of fields to include in the response (default: all)
     * @param integer $rows  Limit of maximum number of results returned, (Default: 0 == unlimited)
     * @param integer $start Offset from which to start returning results (Default: 0 == start at first result)
     * @return mixed (Depends on ResponseWriter output)
     */
    public function search($q, $wt = false, $fl = false, $rows = 0, $start = 0)
    {
        self::getLog()->info('Called search() with phrase: '.$q);
        $wt = mb_strtolower($wt);
        // Open existing index
        $this->_openIndex();
        self::getLog()->debug(sprintf('Index count: %s, number of documents: %s',
                              $this->index->count(), $this->index->numDocs()));
        //self::getLog()->debug('Index opened, passing search result to ResponseWriter...');
        
        $writerTypes = array('xml');
        if ($wt != false && $wt != 'xml' && in_array($wt, $writerTypes)) {
            /* @TODO add check for available ResponseWriter types */
        } else {
            $writer = new Indexer_Service_ResponseWriter_XML();
        }
        if ($fl != false) {
            $writer->includeFields(explode(',', $fl));
        }
        
        $proxy = new Indexer_Service_ResultProxy_ZendSearchLucene($this->index->find($q));
        
        if (is_numeric($rows) && $rows > 0) {
            $proxy->setLimit($rows);
        }
        if (is_numeric($start) && $start > 0) {
            $proxy->setOffset($start);
        }
        
        $writer->setResponse($proxy);
        
        $hits = $writer->getResponseObject();
        
        //self::getLog()->debug('Returning result...');
        return $hits;
    }
    
    /**
     * Get time of last modification for remote files. Function taken from
     * user comments at www.php.net/filemtime
     * @param string $uri
     * @return int
     */
    private function getRemoteLastModified($uri)
    {
        // default
        $unixtime = 0;
       
        $fp = fopen($uri, "r");
        if(!$fp) {
            return;
        }
       
        $MetaData = stream_get_meta_data( $fp );
           
        foreach($MetaData['wrapper_data'] as $response) {
            // case: redirection
            if(substr(strtolower($response), 0, 10) == 'location: ') {
                $newUri = substr($response, 10);
                //self::getLog()->debug('Redirect found at getRemoteLastModified('.$uri.'), redirecting to: '.$newUri);
                fclose($fp);
                return $this->getRemoteLastModified($newUri);
            }
            // case: last-modified
            elseif(substr(strtolower($response), 0, 15) == 'last-modified: ') {
                $unixtime = strtotime(substr($response, 15));
                break;
            }
        }
        fclose($fp);
        if (!$unixtime) {
            return time();
        }
        return $unixtime;
    }
    
    protected function _addDocument($documentList)
    {
        self::getLog()->debug('Called addDocument...');
        $this->_openIndex();
        if (is_string($documentList)) {
            $documentList = array($documentList);
        }
        foreach($documentList as $doc) {
            //self::getLog()->debug('Starting new document...');
            if ($doc instanceof SimpleXMLElement) {
                //self::getLog()->debug('Document is SimpleXMLElement');
                if (!empty($doc['src'])) {
                    $doc = (string)$doc['src'];
                    self::getLog()->debug("Creating document from URI: $doc!");
                } elseif(!empty($doc->field)) {
                    $doc = Indexer_Document::fromXML($doc);
                    self::getLog()->debug('Created Indexer_Document from XML');
                }
            }
            if (is_string($doc)) {
                //self::getLog()->debug('The document seems to be a string, trying to retrieve as file');
                // it's most likely we should get a remote file
                $http = new Zend_Http_Client($doc);
                $response = $http->request();
                if ($response->isError()) {
                    self::getLog()->err("HTTP request for '$doc' failed: [Code ".
                                        $response->getStatus().'] '.$response->getMessage());
                    continue;
                }
                if (substr($response->getHeader('Content-type'), 0, 9) != 'text/html') {
                    $msg = "Error when retrieving '$doc': Content type is not text/html.".
                           ' Actual type: '.$response->getHeader('Content-type');
                    self::getLog()->crit($msg);
                    throw new Indexer_Exception($msg);
                }
                $doc = Indexer_Document::fromHTML($doc, $response->getBody());
            }
            if (!($doc instanceOf Indexer_Document)) {
                $msg = 'Document is not of type Indexer_Document!';
                self::getLog()->crit($msg);
                throw new Indexer_Exception($msg);
            }
            //error_log('Document content: '.print_r($doc,true));
            /* @var $doc Indexer_Document */
            $indexDoc = new Zend_Search_Lucene_Document();
            // Store document URL to identify it in search result.
            $indexDoc->addField(Zend_Search_Lucene_Field::Keyword('uri', $doc->getURI()));
            
            foreach($doc->getFieldNames() as $fieldName) {
                //self::getLog()->debug('Adding field: '.$fieldName);
                $field = null;
                switch($doc->getIndexType($fieldName)) {
                    case Indexer_Document::INDEX_TYPE_UNINDEXED:
                        //self::getLog()->debug("Adding $fieldName as UnIndexed");
                        $field = Zend_Search_Lucene_Field::UnIndexed($fieldName, $doc->getField($fieldName), $this->encoding);
                        break;
                    case Indexer_Document::INDEX_TYPE_UNSTORED:
                        //self::getLog()->debug("Adding $fieldName as UnStored");
                        $field = Zend_Search_Lucene_Field::UnStored($fieldName, $doc->getField($fieldName), $this->encoding);
                        break;
                    case Indexer_Document::INDEX_TYPE_BINARY:
                        //self::getLog()->debug("Adding $fieldName as Binary");
                        $field = Zend_Search_Lucene_Field::Binary($fieldName, $doc->getField($fieldName), $this->encoding);
                        break;
                    case Indexer_Document::INDEX_TYPE_KEYWORD:
                        //self::getLog()->debug("Adding $fieldName as Keyword");
                        $field = Zend_Search_Lucene_Field::Keyword($fieldName, $doc->getField($fieldName), $this->encoding);
                        break;
                    case Indexer_Document::INDEX_TYPE_TEXT:
                    default:
                        //self::getLog()->debug("Adding $fieldName as Text (default)");
                        $field = Zend_Search_Lucene_Field::Text($fieldName, $doc->getField($fieldName), $this->encoding);
                }
                $indexDoc->addField($field);
            }
            
            self::getLog()->debug('Adding document to index... '); //.print_r($doc,true));
            // Add document to the index.
            try {
                $this->index->addDocument($indexDoc);
            } catch(Exception $ex) {
                self::getLog()->err('Error on addDocument: '.$ex->getMessage());
                throw $ex;
            }
        }
        return true;
    }
    
    public function updateDocument($documentList)
    {
        self::getLog()->debug('Called updateDocument...');
        $this->_openIndex();
        /*
    		Lucene index file format doesn't support document updating. 
            Document should be removed and re-added to do this.  
		*/
        $this->removeDocument($documentList);
        $this->_addDocument($documentList);
    }
    
    /**
     * Removes documents from the index. Expects an array with document
     * URIs (or document objects, or SimpleXML elements containing URIs).
     *
     * @param array $documentList
     */
    public function removeDocument($documentList)
    {
        $this->_openIndex();
        if (is_string($documentList)) {
            $documentList = array($documentList);
        }
        foreach($documentList as $uri) {
            if (!is_string($uri)) {
                if (!empty($uri['src'])) {
                    // call from updateDocument with <doc src="" />
                    $uri = (string)$uri['src'];
                } elseif(!empty($uri->field)) {
                    // call from updateDocument with <doc><field name="uri">...</field>...</doc>
                    foreach($uri->field as $field) {
                        $name = (string)$field['name'];
                        if ($name == 'uri') {
                            $uri = (string)$field;
                            break;
                        }
                    }
                }
            }
            // everything else should either be a direct URI or an XML document
            // of the format <delete><uri>...</uri>...</delete>
            $uri = (string)$uri;
            self::getLog()->debug("Called removeDocument with URI: '$uri'");
            $term = new Zend_Search_Lucene_Index_Term($uri, 'uri');
            $docIds  = $this->index->termDocs($term);
            $i = 0;
            foreach ($docIds as $id) {
                //self::getLog()->debug("Deleting hit: ".$id);
                $this->index->delete($id);
                $i++;
            }
            self::getLog()->debug("Deleted $i hits");
        }
        /* @TODO implement delete by query: <delete><query>...</query>...</delete> */
    }
    
}